Cleveland’s Changing Population

Exploring the US Census with R

Published

January 27, 2025

The resurgence of people moving to downtown Cleveland is making news.1 According to a study commissioned by Downtown Cleveland Inc., the downtown population was almost 19,000 in the 2020 census, a 22% increase from 2010.2 However, Cleveland Open Data shows only 13,0003. Cleveland Scene reports that there are lots of estimates out there, one as low as 8,000!4 What gives? The organizations may be using different sources, like the decennial US census vs the more recent, but less comprehensive, American Community Survey. But it seems more likely they are using different geographic boundaries.

I was able to reproduce some estimates. My main tools to do this was the tidycensus R package, and the Cleveland Open Data service. I’ve stepped through the process below.

Note

This web page is a working file. It’s mostly a toy project to learn cool R package, learn how to retrieve data from the US Census Bureau API, and leave a record I can refer to later. If you are reading this and are not me, I hope this helps with whatever you’re doing. Otherwise, ‘hello, future me!’ You can find the source code and downloaded data on my GitHub page.

Defining “Downtown”

Cleveland extends from Cleveland Hopkins Airport on the west all the way to Euclid on the east. It’s mostly bounded on the south by I-80. Here is the map from the Cleveland Wikipedia Page.

The 2020 US decennial census counted 372K people in Cleveland.5 That’s a decline from 397K in 2010. The 1-year American Community Survey (ACS) shows it is still falling, down to 363K in 2023.6 But the decline is uneven, and parts of the city are actually growing, including the downtown area. There is no official definition of downtown, so we can make some choices. The Census Bureau provides the building blocks for a definition: over 15K census blocks, rolled up to around 200 census tracts.

The Cleveland City Planning Commission defines 34 neighborhoods in Cleveland for urban planning initiatives.7 Cleveland Open Data has an interactive map that you can explore and download. There is a pdf map at the CPC that I copy/pasted below. You can see from the map that there is a neighborhood actually called “Downtown”. It’s the area bounded by the Cuyahoga and I-90.

So that is one definition. A second comes from a study commission by Downtown Cleveland, Inc.. Urban Partners published a study of the Downtown Cleveland market in 2023. Page 3 of the pdf report shows a Westside and a Downtown Core. Whereas the Downtown neighborhood had about 13.3K people in the 2020 census, this definition has 18.7K in the Downtown Core. Notice how the Downtown Core takes a bite out of the Central neighborhood on the east side, and parts of the West Bank of the Flats in the Cuyahoga Valley neighborhood and Ohio City neighborhood.

I think I’ll stick with the CPC definitions, but keep the Ohio City, Central, and other neighborhoods in mind.

Blocks, Tracts, and Subdivisions

Several libraries make it easy to work with census data. The tidycencus package was developed to interface with the US Census Bureau APIs. It also returns feature geometry for spatial analysis. The tigris package works with the Census Bureau’s TIGER/Line shapefiles, and the sf (simple features) package to perform spatial operations.

library(tidyverse)
library(glue)
library(scales)
library(gt)
library(ggiraph)  # interactive plots

library(tidycensus)
library(tigris) # TIGER/Line shapefiles
library(sf)  # simple features for spatial analysis

My first step is to get a precise definition of Cleveland’s neighborhoods. The City of Cleveland Open Data web site has an interactive analysis of the 2020 US Census.8 The map has five layers. The blocks file includes the census block and tract, plus the CPC’s neighborhood identifier.

# Cleveland populated blocks. Includes block, tract, and SPA name.
cleve_blocks <- st_read(file.path(
  "inputs/Cleveland Populated Blocks 2020",
  "Decennial_2020_Populated_Blocks_Cleveland_Only.shp"
)) |>
  select(-starts_with("P0"), -starts_with("H0"))

# Nice contiguous shape file. No census bock/tract/etc. here though.
cleve_neigh_0 <-
  st_read(file.path(
  "inputs/Cleveland Neighborhoods",
  "Neighborhood_Population_Change.shp"
))

I can use this along with other shape data from the tigris package to define and map Cleveland and its neighborhoods.

Show the code
oh_state <- tigris::states(cb = TRUE) |> filter(STUSPS == "OH") 

oh_counties <- tigris::counties(cb = TRUE) |> filter(STUSPS == "OH")
cuya_county <- oh_counties |> filter(NAME == "Cuyahoga")

oh_places <- tigris::places("OH", year = 2022)
my_places <-
  oh_places |> 
  filter(NAME %in% c(
    "Euclid", "Parma", "North Olmsted", "Shaker Heights", "Solon", "Lakewood")) |>
  st_centroid()

terminal_tower <- st_sfc(st_point(c(-81.69387, 41.49824)), crs = 4326)
Show the code
p <-
  ggplot() +
  geom_sf(data = oh_state, color = "gray60") +
  geom_sf_interactive(
    data = oh_counties, 
    aes(tooltip = NAME),
    fill = "honeydew", color = "gray90"
  ) +
  geom_sf(data = cuya_county, fill = "honeydew2", color = "gray80") +
  geom_sf_interactive(
    data = cleve_neigh_0,
    aes(tooltip = SPA_NAME),
    fill = "honeydew3", color = "honeydew4"
  ) +
  geom_sf(data = my_places, color = "honeydew3") +
  geom_sf(data = terminal_tower, color = "firebrick") +
  geom_sf_text(data = terminal_tower, aes(label = "Terminal Tower"),
               size = 3, hjust = .2, vjust = 1, color = "firebrick") +
  geom_sf_text(data = my_places, aes(label = NAME), size = 3, hjust = .2, vjust = 1) +
  coord_sf(xlim = c(-82.0, -81.3), ylim = c(41.25, 41.65)) +
  theme(
    panel.background = element_rect(fill = "skyblue"),
    panel.grid = element_blank(),
    axis.text = element_blank()
  ) +
  labs(
    x = NULL, y = NULL, 
    title = glue("Cleveland and Surrounding Cities, Cuyahoga County")
  )

girafe(ggobj = p)

Census Data

I don’t want to abuse the US Census Bureau API, so I’ll set a flag to only download data as I’m developing this script. Once I have what I want, I’ll keep my data on my local drive and build my report.

USE_API <- FALSE

The Census Bureau API allows you to select multiple variables from a single census file. There are a few files for each census, and the variable names change. I want the Cleveland area population in 2000, 2010, 2020, and the American Community Survey (ACS) 1-year estimate from 2023 (most recent). So despite the handiness of tidycensus package, data collection is still going to be a bit tedious.

The decennial census developer page lists the accessible datasets: 2000, 2010, and 2020. Before you do anything, you’ll need an API key from the Bureau. This is quick and easy: just click the “Request a KEY” tile in the menu at the left. The Census Bureau emails you a key. Best practice is to save the key in an .Renviron file.

usethis::edit_r_environ(scope = "project")

This opens (or creates) a .Renviron file in your project root. Add your key. The name is important: CENSUS_API_KEY. The tidycensus functions send that system variable (if you don’t explicitly supply it in the function). Set it like this:

CENSUS_API_KEY="abc123"

Now we’re ready to pull census data. I’ll start with 2020.

2020

Through trial and error, I discovered the Redistricting Data (PL 94-171) contains overall population. There is a full list of variables that represent the various sub-groups of the population. I used it and the tidycensus::load_variables() function to identify the ones I want. I’ll include race/ethnicity to investigate demographic trends.

pl_2020_vars <-
  tidycensus::load_variables(2020, "pl") |>
  filter(
    between(name, "P2_001N", "P2_011N"),
    !name %in% c("P2_003N", "P2_004N")
  )

Here they are after a bit of cleaning.

Show the code
pl_2020_vars <- 
  pl_2020_vars |>
  mutate(
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some Other Race") ~ "Other",
      str_detect(label, "two or more races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label
    ),
    rpt_group = if_else(name == "P2_001N", "Total", "Race/ethnicity"),
    rpt_level = if_else(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total"),
      label, "Other")
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

pl_2020_vars
# A tibble: 9 × 4
  variable label             rpt_group      rpt_level
  <chr>    <chr>             <chr>          <chr>    
1 P2_001N  Total             Total          Total    
2 P2_002N  Hispanic          Race/ethnicity Hispanic 
3 P2_005N  White             Race/ethnicity White    
4 P2_006N  Black             Race/ethnicity Black    
5 P2_007N  American Indian   Race/ethnicity Other    
6 P2_008N  Asian             Race/ethnicity Asian    
7 P2_009N  Pacific Islander  Race/ethnicity Other    
8 P2_010N  Other             Race/ethnicity Other    
9 P2_011N  Two or more races Race/ethnicity Other    

The Demographic Profile contains age counts, so I’ll grab that too since I am curious about general age patterns.

dp_2020_vars <- 
  tidycensus::load_variables(2020, "dp") |>
  filter(
    str_detect(label, "Count!!SEX AND AGE!!Total population"),
    !str_detect(label, "Selected Age Categories"),
    name != "DP1_0001C"
  )

Clean these too.

Show the code
dp_2020_vars <- 
  dp_2020_vars |>
  mutate(
    label = str_remove_all(label, "(Count!!SEX AND AGE!!Total population)|(!!)"),
    label = if_else(label == "", "Total", label),
    rpt_group = "Age",
    rpt_level = case_when(
      name <= "DP1_0004C" ~ "Under 15 yrs",
      name <= "DP1_0006C" ~ "15 to 24 yrs",
      name <= "DP1_0008C" ~ "25 to 34 yrs",
      name <= "DP1_0010C" ~ "35 to 44 yrs",
      name <= "DP1_0012C" ~ "45 to 54 yrs",
      name <= "DP1_0014C" ~ "55 to 64 yrs",
      TRUE ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

dp_2020_vars
# A tibble: 18 × 4
   variable  label             rpt_group rpt_level   
   <chr>     <chr>             <chr>     <chr>       
 1 DP1_0002C Under 5 years     Age       Under 15 yrs
 2 DP1_0003C 5 to 9 years      Age       Under 15 yrs
 3 DP1_0004C 10 to 14 years    Age       Under 15 yrs
 4 DP1_0005C 15 to 19 years    Age       15 to 24 yrs
 5 DP1_0006C 20 to 24 years    Age       15 to 24 yrs
 6 DP1_0007C 25 to 29 years    Age       25 to 34 yrs
 7 DP1_0008C 30 to 34 years    Age       25 to 34 yrs
 8 DP1_0009C 35 to 39 years    Age       35 to 44 yrs
 9 DP1_0010C 40 to 44 years    Age       35 to 44 yrs
10 DP1_0011C 45 to 49 years    Age       45 to 54 yrs
11 DP1_0012C 50 to 54 years    Age       45 to 54 yrs
12 DP1_0013C 55 to 59 years    Age       55 to 64 yrs
13 DP1_0014C 60 to 64 years    Age       55 to 64 yrs
14 DP1_0015C 65 to 69 years    Age       65+ yrs     
15 DP1_0016C 70 to 74 years    Age       65+ yrs     
16 DP1_0017C 75 to 79 years    Age       65+ yrs     
17 DP1_0018C 80 to 84 years    Age       65+ yrs     
18 DP1_0019C 85 years and over Age       65+ yrs     

With the variable names in hand, we can request the data from the API. Cleveland is one of 59 subdivisions within Cuyahoga County. Counties are subdivided into census tracts, and census tracts are subdivided into census blocks. Cities often overlap census tracts, so I’ll defined Cleveland by joining to the cleve_blocks data I got from Cleveland Open Data.

Show the code
# Utility function to create factors
my_rpt_relevel <- function(x) {
  ethn <- c("Black", "White", "Hispanic", "Asian", "Other", "Total")
  x <- fct_relevel(x, ethn, after = Inf)
  x <- fct_relevel(x, "Under 15 yrs", after = 0)
  return(x)
}

# Urban Partners defn of Westside, uses tracts.
westside_tracts <- c(
  "103100", "103400", "103500", "103602", "103800", "103900", "104100", 
  "104200", "104300", "197800", "197700", "197500", "104400"
)

# Urban Partners defn of Downtown Core, uses tracts and blocks
downtown_core_tracts_2020 <- c(
  "103300", "107101", "107701", "107802", "109301")
downtown_core_blocks_2020 <- paste0(
  "39035108701", c("2001", "2004", "2006", "2008"))

if (USE_API) {

  subdiv_2020_pl <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "pl",
      variables = pl_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(pl_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  subdiv_2020_dp <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "dp",
      variables = dp_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(dp_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  tract_2020_pl <- 
    get_decennial( 
      geography = "tract",
      sumfile = "pl",
      variables = pl_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(pl_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  tract_2020_dp <- 
    get_decennial( 
      geography = "tract",
      sumfile = "dp",
      variables = dp_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |>
    inner_join(dp_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  block_2020_pl <- 
    get_decennial( 
      geography = "block",
      sumfile = "pl",
      variables = pl_2020_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2020
    ) |> 
    inner_join(pl_2020_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    )
  
  # dp is not available at the block level

  subdiv_2020 <- 
    bind_rows(subdiv_2020_pl, subdiv_2020_dp) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
           
  tract_2020 <-
    bind_rows(tract_2020_pl, tract_2020_dp) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))
  
  block_2020 <-
    block_2020_pl |>
    inner_join(
      cleve_blocks |> as_tibble() |> select(GEOID20, SPA = SPA_NAME), 
      by = c("GEOID" = "GEOID20")
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      greater_downtown = case_when(
        str_sub(GEOID, 6, 11) %in% westside_tracts ~ "Westside",
        str_sub(GEOID, 6, 11) %in% downtown_core_tracts ~ "Downtown Core",
        GEOID %in% downtown_core_blocks_2020 ~ "Downtown Core",
        TRUE ~ "Other"
      ),
      SPA = factor(str_to_title(SPA)),
      SPA = fct_relevel(SPA, "Downtown", after = 0),
      greater_downtown = factor(
        greater_downtown, levels = c("Downtown Core", "Westside", "Other"))
    )
  
  save(
    subdiv_2020, tract_2020, block_2020,
    file = "decennial_2020.Rdata"
  )

} else {
  
  load("decennial_2020.Rdata")
  
}

Cleveland’s population was 372,624 in 2020. The Downtown neighborhood had 13,302 people. The Downtown Core, which included a portion of the Central neighborhood, had 18,708 people.

2020 Population Estimates for Cleveland and Vicinity
Population
Cuyahoga County Subdivisions
Cleveland 372,624
Other 892,193
Total 1,264,817
Cleveland Neighborhoods
Downtown 13,302
Bellaire-Puritas 13,823
Broadway-Slavic Village 19,022
Brooklyn Centre 8,315
Buckeye Shaker 11,419
Central 11,955
Clark Fulton 7,625
Collinwood Nottingham 9,616
Cudell 9,115
Cuyahoga Valley 1,293
Detroit-Shoreway 11,326
Edgewater 6,000
Euclid Green 5,051
Fairfax 5,167
Glenville 21,137
Goodrich-Kirtland Park 3,955
Hopkins 534
Hough 9,702
Jefferson 17,351
Kamms Corners 24,312
Kinsman 5,876
Lee-Harvard 9,770
Lee-Seville 4,171
Mount Pleasant 14,015
North Shore Collinwood 14,928
Ohio City 9,219
Old Brooklyn 32,315
Saint Clair-Superior 5,139
Stockyards 9,522
Tremont 7,798
Union-Miles Park 15,625
University Circle 9,620
West Boulevard 18,981
Woodland Hills 5,625
Total 372,624
Greater Downtown
Downtown Core 18,708
Westside 18,407
Other 335,509
Total 372,624

2010

Unfortunately, getting 2010 and 2000 isn’t as simple as changing the year parameter in the API calls because they use a different file - Summary File 1.

sf1_2010_vars <-
  tidycensus::load_variables(2010, "sf1") |>
  filter(
    concept %in% c("HISPANIC OR LATINO ORIGIN BY RACE", "SEX BY AGE"),
    !name %in% c("P005002", "P012001", "P012002", "P012026"),
    !str_detect(label, "Total!!Hispanic or Latino!!"),
    !str_detect(name, "^PCT012")
  )
Show the code
sf1_2010_vars <-
  sf1_2010_vars |>
  mutate(
    label = str_remove(label, "(Total!!Male!!)|(Total!!Female!!)"),
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some Other Race") ~ "Other",
      str_detect(label, "Two or More Races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label,
    ),
    rpt_group = case_when(
      name == "P005001" ~ "Total",
      between(name, "P005003", "P005010") ~ "Race/ethnicity",
      TRUE ~ "Age"
    ),
    rpt_level = case_when(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total", "Other") ~ label,
      label %in% c("American Indian", "Pacific Islander", "Two or more races") ~ "Other",
      label %in% c("Under 5 years", "5 to 9 years", "10 to 14 years") ~ "Under 15 yrs",
      between(label, "15 to 17 years", "22 to 24 years") ~ "15 to 24 yrs",
      label %in% c("25 to 29 years", "30 to 34 years") ~ "25 to 34 yrs",
      label %in% c("35 to 39 years", "40 to 44 years") ~ "35 to 44 yrs",
      label %in% c("45 to 49 years", "50 to 54 years") ~ "45 to 54 yrs",
      between(label, "55 to 59 years", "62 to 64 years") ~ "55 to 64 yrs",
      between(label, "65 and 66 years", "85 years and over") ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

sf1_2010_vars
# A tibble: 55 × 4
   variable label             rpt_group      rpt_level   
   <chr>    <chr>             <chr>          <chr>       
 1 P005001  Total             Total          Total       
 2 P005003  White             Race/ethnicity White       
 3 P005004  Black             Race/ethnicity Black       
 4 P005005  American Indian   Race/ethnicity Other       
 5 P005006  Asian             Race/ethnicity Asian       
 6 P005007  Pacific Islander  Race/ethnicity Other       
 7 P005008  Other             Race/ethnicity Other       
 8 P005009  Two or more races Race/ethnicity Other       
 9 P005010  Hispanic          Race/ethnicity Hispanic    
10 P012003  Under 5 years     Age            Under 15 yrs
# ℹ 45 more rows

Request the data from the API.

Show the code
downtown_core_tracts_2010 <- c(
  "103300", "107101", "107701", "107802", "109301")
downtown_core_blocks_2010 <- 
  paste0("39035108701", c("3000", "3001", "3002", "3003", "3004"))

if (USE_API) {

  subdiv_2010 <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "sf1",
      variables = sf1_2010_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2010
    ) |> 
    inner_join(sf1_2010_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
  
  tract_2010 <- 
    get_decennial( 
      geography = "tract",
      sumfile = "sf1",
      variables = sf1_2010_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2010
    ) |> 
    inner_join(sf1_2010_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2010_0 <- 
    get_decennial( 
      geography = "block",
      sumfile = "sf1",
      variables = sf1_2010_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2010
    ) |> 
    inner_join(sf1_2010_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2010 <-
    st_join(cleve_neigh, st_centroid(block_2010_0), join = st_contains) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      greater_downtown = case_when(
        str_sub(GEOID, 6, 11) %in% westside_tracts ~ "Westside",
        str_sub(GEOID, 6, 11) %in% downtown_core_tracts_2010 ~ "Downtown Core",
        GEOID %in% downtown_core_blocks_2010 ~ "Downtown Core",
        TRUE ~ "Other"
      ),
      SPA = factor(str_to_title(SPA)),
      SPA = fct_relevel(SPA, "Downtown", after = 0),
      greater_downtown = factor(
        greater_downtown, levels = c("Downtown Core", "Westside", "Other"))
    ) |>
    select(GEOID, NAME, geometry, rpt_group, rpt_level, value, SPA, greater_downtown)
  
  save(
    subdiv_2010, tract_2010, block_2010,
    file = "decennial_2010.Rdata"
  )

} else {
  
  load("decennial_2010.Rdata")
  
}

The sum of the neighborhoods, 395,601, didn’t quite roll match the city, 396,815. There must be city blocks whose centers are not captured in the neighborhoods shape. I haven’t thought of a good way to ferret them out, so I think I’ll just let this go.

The Downtown population was only 9,464, so it has indeed grown quite a bit from 2010 to 2020.

2010 Population Estimates for Cleveland and Vicinity
Population
Cuyahoga County Subdivisions
Cleveland 396,815
Other 883,307
Total 1,280,122
Cleveland Neighborhoods
Downtown 9,464
Bellaire-Puritas 13,380
Broadway-Slavic Village 22,331
Brooklyn Centre 8,948
Buckeye Shaker 12,470
Central 12,306
Clark Fulton 8,509
Collinwood Nottingham 11,542
Cudell 9,295
Cuyahoga Valley 1,378
Detroit-Shoreway 11,577
Edgewater 5,851
Euclid Green 4,873
Fairfax 6,239
Glenville 27,394
Goodrich-Kirtland Park 4,238
Hopkins 646
Hough 11,490
Jefferson 16,548
Kamms Corners 24,097
Kinsman 6,966
Lee-Harvard 10,326
Lee-Seville 4,477
Mount Pleasant 17,320
North Shore Collinwood 15,768
Ohio City 8,396
Old Brooklyn 32,009
Saint Clair-Superior 6,876
Stockyards 10,411
Tremont 7,975
Union-Miles Park 19,004
University Circle 7,939
West Boulevard 18,880
Woodland Hills 6,678
Total 395,601
Greater Downtown
Downtown Core 15,156
Westside 18,433
Other 362,012
Total 395,601

2000

2000 is similar to 2010 in that it uses Summary File 1.

sf1_2000_vars <-
  tidycensus::load_variables(2000, "sf1") |>
  filter(
    concept %in% c(
      "HISPANIC OR LATINO, AND NOT HISPANIC OR LATINO BY RACE [73]", 
      "SEX BY AGE [49]"
    ),
    !name %in% c("P004003", "P004004", "P012001", "P012002", "P012026"),
    !str_detect(label, "Population of two or more races!!"),
    !str_detect(name, "^PCT013")
  )
Show the code
sf1_2000_vars <-
  sf1_2000_vars |>
  mutate(
    label = str_remove(label, "(Total!!Male!!)|(Total!!Female!!)"),
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some Other Race") ~ "Other",
      str_detect(label, "Two or More Races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label,
    ),
    rpt_group = case_when(
      name == "P004001" ~ "Total",
      between(name, "P004002", "P004011") ~ "Race/ethnicity",
      TRUE ~ "Age"
    ),
    rpt_level = case_when(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total", "Other") ~ label,
      label %in% c("American Indian", "Pacific Islander", "Two or more races") ~ "Other",
      label %in% c("Under 5 years", "5 to 9 years", "10 to 14 years") ~ "Under 15 yrs",
      between(label, "15 to 17 years", "22 to 24 years") ~ "15 to 24 yrs",
      label %in% c("25 to 29 years", "30 to 34 years") ~ "25 to 34 yrs",
      label %in% c("35 to 39 years", "40 to 44 years") ~ "35 to 44 yrs",
      label %in% c("45 to 49 years", "50 to 54 years") ~ "45 to 54 yrs",
      between(label, "55 to 59 years", "62 to 64 years") ~ "55 to 64 yrs",
      between(label, "65 and 66 years", "85 years and over") ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

sf1_2010_vars
# A tibble: 55 × 4
   variable label             rpt_group      rpt_level   
   <chr>    <chr>             <chr>          <chr>       
 1 P005001  Total             Total          Total       
 2 P005003  White             Race/ethnicity White       
 3 P005004  Black             Race/ethnicity Black       
 4 P005005  American Indian   Race/ethnicity Other       
 5 P005006  Asian             Race/ethnicity Asian       
 6 P005007  Pacific Islander  Race/ethnicity Other       
 7 P005008  Other             Race/ethnicity Other       
 8 P005009  Two or more races Race/ethnicity Other       
 9 P005010  Hispanic          Race/ethnicity Hispanic    
10 P012003  Under 5 years     Age            Under 15 yrs
# ℹ 45 more rows

Request the data from the API.

Show the code
downtown_core_tracts_2000 <- c(
  "107100", "107200", "107300", "107400", "107500", "107600", "107700",
  "107800", "107900", "109200")
  # "103300", "107101", "107701", "107802", "109301")
downtown_core_blocks_2000 <- 
  paste0("39035108701", c("3000", "3001", "3002", "3003", "3004"))

if (USE_API) {

  subdiv_2000_0 <- 
    get_decennial( 
      geography = "county subdivision",
      sumfile = "sf1",
      variables = sf1_2000_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = FALSE, # no county subdivision geography in 2000
      year = 2000
    ) |> 
    inner_join(sf1_2000_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
  
  # No geometry for 2000? No problem? I'll use the 2010 geometry and replace the 
  # values with 2000.
  subdiv_2000_1 <- 
    subdiv_2000_0 |> 
    as_tibble() |> 
    select(GEOID, rpt_group, rpt_level, value)
  
  subdiv_2000 <- 
    subdiv_2010 |>
    select(-value) |>
    inner_join(subdiv_2000_1, by = c("GEOID", "rpt_group", "rpt_level"))
  
  tract_2000 <- 
    get_decennial( 
      geography = "tract",
      sumfile = "sf1",
      variables = sf1_2000_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2000
    ) |> 
    inner_join(sf1_2000_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2000_0 <- 
    get_decennial( 
      geography = "block",
      sumfile = "sf1",
      variables = sf1_2000_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = TRUE, 
      year = 2000
    ) |> 
    inner_join(sf1_2000_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, geometry, rpt_group, rpt_level),
      value = sum(value)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  block_2000 <-
    st_join(cleve_neigh, st_centroid(block_2000_0), join = st_contains) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      greater_downtown = case_when(
        str_sub(GEOID, 6, 11) %in% westside_tracts ~ "Westside",
        str_sub(GEOID, 6, 11) %in% downtown_core_tracts_2000 ~ "Downtown Core",
        GEOID %in% downtown_core_blocks_2000 ~ "Downtown Core",
        TRUE ~ "Other"
      ),
      SPA = factor(str_to_title(SPA)),
      SPA = fct_relevel(SPA, "Downtown", after = 0),
      greater_downtown = factor(
        greater_downtown, levels = c("Downtown Core", "Westside", "Other"))
    ) |>
    select(GEOID, NAME, geometry, rpt_group, rpt_level, value, SPA, greater_downtown)
  
  save(
    subdiv_2000, tract_2000, block_2000,
    file = "decennial_2000.Rdata"
  )

} else {
  
  load("decennial_2000.Rdata")
  
}

We have the same problem as 2010 - the sum of the neighborhoods, 477,107, didn’t quite roll match the city, 478,403.

Wow, Cleveland’s population was 478,403 in 2000 - that’s a 100K more than today! It sure has dropped a lot over the last two decades. On the other hand, only 6,310 people lived downtown. The resurgence of Downtown does not seem to be a recent phenomena.

2000 Population Estimates for Cleveland and Vicinity
Population
Cuyahoga County Subdivisions
Cleveland 478,403
Other 915,575
Total 1,393,978
Cleveland Neighborhoods
Downtown 6,310
Bellaire-Puritas 14,520
Broadway-Slavic Village 30,652
Brooklyn Centre 10,155
Buckeye Shaker 16,063
Central 11,568
Clark Fulton 10,672
Collinwood Nottingham 15,874
Cudell 10,630
Cuyahoga Valley 1,307
Detroit-Shoreway 13,917
Edgewater 6,360
Euclid Green 6,169
Fairfax 8,447
Glenville 39,941
Goodrich-Kirtland Park 4,580
Hopkins 338
Hough 14,734
Jefferson 18,266
Kamms Corners 25,256
Kinsman 10,256
Lee-Harvard 11,665
Lee-Seville 5,595
Mount Pleasant 24,013
North Shore Collinwood 18,346
Ohio City 8,726
Old Brooklyn 34,169
Saint Clair-Superior 11,534
Stockyards 12,076
Tremont 9,317
Union-Miles Park 26,539
University Circle 9,386
West Boulevard 20,492
Woodland Hills 9,234
Total 477,107
Greater Downtown
Downtown Core 8,412
Westside 15,154
Other 453,541
Total 477,107

2023 (ACS)

The 2023 American Community Survey publishes a 1-year and 5-year average. We’ll grab the 1-year to get the latest population figures.

acs1_2023_vars <-
  tidycensus::load_variables(2023, "acs1") |>
  filter(
    concept %in% c("Sex by Age", "Hispanic or Latino Origin by Race"),
    # between(name, "B01001_001E_001N", "P2_011N"),
    !name %in% c("B01001_002", "B01001_026", "B03002_001", "B03002_002",
                 "B03002_010", "B03002_011"),
    name <= "B03002_012"
  )
Show the code
acs1_2023_vars <-
  acs1_2023_vars |>
  mutate(
    label = str_remove_all(label, "(Estimate!!Total:!!)|(Male:!!)|(Female:!!)"),
    label = case_when(
      str_detect(label, "White") ~ "White",
      str_detect(label, "Black") ~ "Black",
      str_detect(label, "Asian") ~ "Asian",
      str_detect(label, "American Indian") ~ "American Indian",
      str_detect(label, "Native Hawaiian") ~ "Pacific Islander",
      str_detect(label, "Some other race") ~ "Other",
      str_detect(label, "Two or more races") ~ "Two or more races",
      str_detect(label, "Hispanic") ~ "Hispanic",
      str_detect(label, "Total") ~ "Total",
      TRUE ~ label,
    ),
    rpt_group = case_when(
      name == "B01001_001" ~ "Total",
      between(name, "B03002_003", "B03002_012") ~ "Race/ethnicity",
      TRUE ~ "Age"
    ),
    rpt_level = case_when(
      label %in% c("White", "Black", "Hispanic", "Asian", "Total", "Other") ~ label,
      label %in% c("American Indian", "Pacific Islander", "Two or more races") ~ "Other",
      label %in% c("Under 5 years", "5 to 9 years", "10 to 14 years") ~ "Under 15 yrs",
      between(label, "15 to 17 years", "22 to 24 years") ~ "15 to 24 yrs",
      label %in% c("25 to 29 years", "30 to 34 years") ~ "25 to 34 yrs",
      label %in% c("35 to 39 years", "40 to 44 years") ~ "35 to 44 yrs",
      label %in% c("45 to 49 years", "50 to 54 years") ~ "45 to 54 yrs",
      between(label, "55 to 59 years", "62 to 64 years") ~ "55 to 64 yrs",
      between(label, "65 and 66 years", "85 years and over") ~ "65+ yrs"
    )
  ) |>
  select(variable = name, label, rpt_group, rpt_level)

acs1_2023_vars
# A tibble: 55 × 4
   variable   label           rpt_group rpt_level   
   <chr>      <chr>           <chr>     <chr>       
 1 B01001_001 Total           Total     Total       
 2 B01001_003 Under 5 years   Age       Under 15 yrs
 3 B01001_004 5 to 9 years    Age       Under 15 yrs
 4 B01001_005 10 to 14 years  Age       Under 15 yrs
 5 B01001_006 15 to 17 years  Age       15 to 24 yrs
 6 B01001_007 18 and 19 years Age       15 to 24 yrs
 7 B01001_008 20 years        Age       15 to 24 yrs
 8 B01001_009 21 years        Age       15 to 24 yrs
 9 B01001_010 22 to 24 years  Age       15 to 24 yrs
10 B01001_011 25 to 29 years  Age       25 to 34 yrs
# ℹ 45 more rows

Request the data from the API.

Show the code
if (USE_API) {

  subdiv_2023 <- 
    get_acs( 
      geography = "county subdivision",
      sumfile = "acs1",
      variables = acs1_2023_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = FALSE, # no geo file for ACS-1yr
      year = 2023
    ) |> 
    inner_join(acs1_2023_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, rpt_group, rpt_level),
      value = sum(estimate)
    ) |>
    mutate(
      rpt_level = my_rpt_relevel(rpt_level),
      NAME = str_remove_all(NAME, "(, Cuyahoga County, Ohio)|(village)|(city)"),
      NAME = str_trim(NAME)
    )
  
  tract_2023 <- 
    get_acs( 
      geography = "tract",
      sumfile = "acs1",
      variables = acs1_2023_vars$variable,
      state = "OH",
      county = "Cuyahoga",
      geometry = FALSE, 
      year = 2023
    ) |> 
    inner_join(acs1_2023_vars, by = "variable") |>
    summarize(
      .by = c(GEOID, NAME, rpt_group, rpt_level),
      value = sum(estimate)
    ) |>
    mutate(rpt_level = my_rpt_relevel(rpt_level))

  save(
    subdiv_2023, tract_2023,
    file = "acs1yr_2023.Rdata"
  )

} else {
  
  load("acs1yr_2023.Rdata")
  
}

Cleveland’s population has continued to decline, down from 372,624 in 2020 to 367,523. The ACS does not have block-level data, so I can’t do the neighborhood breakdown here.

But look at Downtown - it’s continued to increase.

2023 Population Estimates for Cleveland and Vicinity
Population
Cleveland 367,523
Other 881,895
Total 1,249,418
Show the code
# 34 features (neighborhoods).
hood_shp <-
  st_read("Cleveland_Neighborhoods/Cleveland_Neighborhoods.shp") |>
  st_make_valid() |>
  st_transform(st_crs(tract_2000))

hood_2023 <- 
  tract_2023 |>
  inner_join(hood_2020 |> select(GEOID, SPANM) |> unique(), by = "GEOID")

hood_shp <- 
  st_read("Cleveland_Neighborhoods/Cleveland_Neighborhoods.shp") |>
  st_make_valid() |>
  st_transform(st_crs(tract_2000))

hood_2000 <- st_join(hood_shp, st_centroid(tract_2000), join = st_contains)

Race/ethnicity

Show the code
neigh_gt("Race/ethnicity", "Black")
Changing Black Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 3,092 3,356 264 9% 3,215 −141 −4% NA NA NA
Broadway-Slavic Village 7,910 11,576 3,666 46% 10,225 −1,351 −12% NA NA NA
Brooklyn Centre 1,102 1,751 649 59% 1,724 −27 −2% NA NA NA
Buckeye Shaker 12,783 10,064 −2,719 −21% 8,583 −1,481 −15% NA NA NA
Central 10,926 11,505 579 5% 9,997 −1,508 −13% NA NA NA
Clark Fulton 1,000 1,441 441 44% 1,378 −63 −4% NA NA NA
Collinwood Nottingham 12,261 10,021 −2,240 −18% 8,329 −1,692 −17% NA NA NA
Cudell 1,724 2,976 1,252 73% 2,879 −97 −3% NA NA NA
Cuyahoga Valley 609 566 −43 −7% 260 −306 −54% NA NA NA
Detroit-Shoreway 2,487 2,867 380 15% 2,309 −558 −19% NA NA NA
Downtown 3,448 4,140 692 20% 3,430 −710 −17% NA NA NA
Edgewater 742 1,194 452 61% 867 −327 −27% NA NA NA
Euclid Green 5,607 4,524 −1,083 −19% 4,472 −52 −1% NA NA NA
Fairfax 8,002 5,932 −2,070 −26% 4,637 −1,295 −22% NA NA NA
Glenville 38,918 26,516 −12,402 −32% 19,524 −6,992 −26% NA NA NA
Goodrich-Kirtland Park 849 985 136 16% 908 −77 −8% NA NA NA
Hopkins 9 131 122 1,356% 148 17 13% NA NA NA
Hough 14,131 10,896 −3,235 −23% 8,453 −2,443 −22% NA NA NA
Jefferson 1,663 2,604 941 57% 3,106 502 19% NA NA NA
Kamms Corners 1,702 2,503 801 47% 2,229 −274 −11% NA NA NA
Kinsman 9,903 6,696 −3,207 −32% 5,480 −1,216 −18% NA NA NA
Lee-Harvard 11,380 10,012 −1,368 −12% 9,263 −749 −7% NA NA NA
Lee-Seville 5,367 4,317 −1,050 −20% 3,914 −403 −9% NA NA NA
Mount Pleasant 23,394 16,817 −6,577 −28% 13,175 −3,642 −22% NA NA NA
North Shore Collinwood 9,065 10,359 1,294 14% 10,310 −49 −0% NA NA NA
Ohio City 2,159 2,718 559 26% 2,082 −636 −23% NA NA NA
Old Brooklyn 836 2,337 1,501 180% 3,657 1,320 56% NA NA NA
Saint Clair-Superior 8,628 5,347 −3,281 −38% 3,743 −1,604 −30% NA NA NA
Stockyards 1,043 1,729 686 66% 1,605 −124 −7% NA NA NA
Tremont 1,669 1,721 52 3% 1,166 −555 −32% NA NA NA
Union-Miles Park 25,523 18,339 −7,184 −28% 14,566 −3,773 −21% NA NA NA
University Circle 2,767 1,849 −918 −33% 1,696 −153 −8% NA NA NA
West Boulevard 1,964 3,589 1,625 83% 4,234 645 18% NA NA NA
Woodland Hills 8,839 6,409 −2,430 −27% 5,249 −1,160 −18% NA NA NA
Total 241,512 208,208 −33,304 −14% 176,813 −31,395 −15% 169,138 −7,675 −4%
Show the code
neigh_gt("Race/ethnicity", "White")
Changing White Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 9,761 7,479 −2,282 −23% 6,050 −1,429 −19% NA NA NA
Broadway-Slavic Village 20,498 8,822 −11,676 −57% 5,988 −2,834 −32% NA NA NA
Brooklyn Centre 5,937 3,962 −1,975 −33% 3,033 −929 −23% NA NA NA
Buckeye Shaker 2,420 1,602 −818 −34% 1,707 105 7% NA NA NA
Central 320 418 98 31% 522 104 25% NA NA NA
Clark Fulton 4,449 2,789 −1,660 −37% 1,942 −847 −30% NA NA NA
Collinwood Nottingham 3,099 1,145 −1,954 −63% 736 −409 −36% NA NA NA
Cudell 6,220 3,813 −2,407 −39% 3,279 −534 −14% NA NA NA
Cuyahoga Valley 643 714 71 11% 893 179 25% NA NA NA
Detroit-Shoreway 7,516 5,257 −2,259 −30% 5,591 334 6% NA NA NA
Downtown 2,380 4,061 1,681 71% 7,550 3,489 86% NA NA NA
Edgewater 4,926 3,865 −1,061 −22% 4,089 224 6% NA NA NA
Euclid Green 390 223 −167 −43% 186 −37 −17% NA NA NA
Fairfax 221 130 −91 −41% 195 65 50% NA NA NA
Glenville 328 238 −90 −27% 473 235 99% NA NA NA
Goodrich-Kirtland Park 1,825 1,479 −346 −19% 1,077 −402 −27% NA NA NA
Hopkins 299 419 120 40% 229 −190 −45% NA NA NA
Hough 272 245 −27 −10% 417 172 70% NA NA NA
Jefferson 14,155 10,246 −3,909 −28% 8,676 −1,570 −15% NA NA NA
Kamms Corners 21,777 18,685 −3,092 −14% 17,643 −1,042 −6% NA NA NA
Kinsman 168 101 −67 −40% 118 17 17% NA NA NA
Lee-Harvard 99 63 −36 −36% 90 27 43% NA NA NA
Lee-Seville 102 32 −70 −69% 48 16 50% NA NA NA
Mount Pleasant 146 112 −34 −23% 166 54 48% NA NA NA
North Shore Collinwood 8,650 4,853 −3,797 −44% 3,659 −1,194 −25% NA NA NA
Ohio City 4,327 3,657 −670 −15% 5,087 1,430 39% NA NA NA
Old Brooklyn 30,167 24,066 −6,101 −20% 19,307 −4,759 −20% NA NA NA
Saint Clair-Superior 1,904 1,028 −876 −46% 782 −246 −24% NA NA NA
Stockyards 7,203 4,614 −2,589 −36% 3,418 −1,196 −26% NA NA NA
Tremont 4,899 4,094 −805 −16% 4,583 489 12% NA NA NA
Union-Miles Park 502 263 −239 −48% 299 36 14% NA NA NA
University Circle 5,135 4,279 −856 −17% 4,722 443 10% NA NA NA
West Boulevard 13,550 9,098 −4,452 −33% 6,882 −2,216 −24% NA NA NA
Woodland Hills 201 139 −62 −31% 110 −29 −21% NA NA NA
Total 185,641 132,710 −52,931 −29% 119,547 −13,163 −10% 124,183 4,636 4%
Show the code
neigh_gt("Race/ethnicity", "Hispanic")
Changing Hispanic Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,263 1,749 486 38% 2,967 1,218 70% NA NA NA
Broadway-Slavic Village 2,077 1,206 −871 −42% 1,664 458 38% NA NA NA
Brooklyn Centre 3,003 2,869 −134 −4% 2,957 88 3% NA NA NA
Buckeye Shaker 501 178 −323 −64% 281 103 58% NA NA NA
Central 291 205 −86 −30% 962 757 369% NA NA NA
Clark Fulton 5,095 4,013 −1,082 −21% 3,804 −209 −5% NA NA NA
Collinwood Nottingham 455 177 −278 −61% 216 39 22% NA NA NA
Cudell 2,120 1,811 −309 −15% 2,031 220 12% NA NA NA
Cuyahoga Valley 51 49 −2 −4% 73 24 49% NA NA NA
Detroit-Shoreway 3,657 2,886 −771 −21% 2,548 −338 −12% NA NA NA
Downtown 325 313 −12 −4% 707 394 126% NA NA NA
Edgewater 552 507 −45 −8% 510 3 1% NA NA NA
Euclid Green 141 40 −101 −72% 193 153 382% NA NA NA
Fairfax 194 37 −157 −81% 95 58 157% NA NA NA
Glenville 595 202 −393 −66% 356 154 76% NA NA NA
Goodrich-Kirtland Park 656 439 −217 −33% 550 111 25% NA NA NA
Hopkins 17 56 39 229% 105 49 88% NA NA NA
Hough 290 152 −138 −48% 297 145 95% NA NA NA
Jefferson 2,091 2,944 853 41% 3,828 884 30% NA NA NA
Kamms Corners 1,368 1,762 394 29% 2,244 482 27% NA NA NA
Kinsman 161 58 −103 −64% 94 36 62% NA NA NA
Lee-Harvard 160 83 −77 −48% 113 30 36% NA NA NA
Lee-Seville 117 34 −83 −71% 69 35 103% NA NA NA
Mount Pleasant 440 145 −295 −67% 194 49 34% NA NA NA
North Shore Collinwood 517 197 −320 −62% 305 108 55% NA NA NA
Ohio City 2,135 1,720 −415 −19% 1,427 −293 −17% NA NA NA
Old Brooklyn 2,691 4,414 1,723 64% 7,180 2,766 63% NA NA NA
Saint Clair-Superior 943 340 −603 −64% 394 54 16% NA NA NA
Stockyards 3,630 3,645 15 0% 3,888 243 7% NA NA NA
Tremont 2,611 1,873 −738 −28% 1,518 −355 −19% NA NA NA
Union-Miles Park 468 117 −351 −75% 277 160 137% NA NA NA
University Circle 364 179 −185 −51% 554 375 209% NA NA NA
West Boulevard 4,404 5,027 623 14% 6,185 1,158 23% NA NA NA
Woodland Hills 176 54 −122 −69% 113 59 109% NA NA NA
Total 43,648 39,534 −4,114 −9% 48,699 9,165 23% 47,132 −1,567 −3%
Show the code
neigh_gt("Race/ethnicity", "Asian")
Changing Asian Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 344 335 −9 −3% 645 310 93% NA NA NA
Broadway-Slavic Village 83 52 −31 −37% 33 −19 −37% NA NA NA
Brooklyn Centre 56 53 −3 −5% 83 30 57% NA NA NA
Buckeye Shaker 332 419 87 26% 404 −15 −4% NA NA NA
Central 9 20 11 122% 78 58 290% NA NA NA
Clark Fulton 86 45 −41 −48% 100 55 122% NA NA NA
Collinwood Nottingham 15 9 −6 −40% 15 6 67% NA NA NA
Cudell 523 310 −213 −41% 332 22 7% NA NA NA
Cuyahoga Valley 1 34 33 3,300% 11 −23 −68% NA NA NA
Detroit-Shoreway 177 149 −28 −16% 264 115 77% NA NA NA
Downtown 145 731 586 404% 1,118 387 53% NA NA NA
Edgewater 104 109 5 5% 186 77 71% NA NA NA
Euclid Green 8 6 −2 −25% 6 0 0% NA NA NA
Fairfax 10 50 40 400% 90 40 80% NA NA NA
Glenville 31 34 3 10% 125 91 268% NA NA NA
Goodrich-Kirtland Park 1,243 1,263 20 2% 1,234 −29 −2% NA NA NA
Hopkins 13 8 −5 −38% 28 20 250% NA NA NA
Hough 22 29 7 32% 157 128 441% NA NA NA
Jefferson 300 305 5 2% 728 423 139% NA NA NA
Kamms Corners 358 642 284 79% 1,044 402 63% NA NA NA
Kinsman 9 9 0 0% 10 1 11% NA NA NA
Lee-Harvard 11 12 1 9% 14 2 17% NA NA NA
Lee-Seville 1 2 1 100% 3 1 50% NA NA NA
Mount Pleasant 9 6 −3 −33% 20 14 233% NA NA NA
North Shore Collinwood 71 34 −37 −52% 48 14 41% NA NA NA
Ohio City 59 82 23 39% 218 136 166% NA NA NA
Old Brooklyn 402 414 12 3% 392 −22 −5% NA NA NA
Saint Clair-Superior 30 16 −14 −47% 21 5 31% NA NA NA
Stockyards 114 81 −33 −29% 78 −3 −4% NA NA NA
Tremont 70 75 5 7% 190 115 153% NA NA NA
Union-Miles Park 14 15 1 7% 14 −1 −7% NA NA NA
University Circle 1,100 1,418 318 29% 2,200 782 55% NA NA NA
West Boulevard 483 440 −43 −9% 491 51 12% NA NA NA
Woodland Hills 7 2 −5 −71% 10 8 400% NA NA NA
Total 6,284 7,213 929 15% 10,390 3,177 44% 8,356 −2,034 −20%

Age

Changing Under 15 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 3,133 2,754 −379 −12% NA NA NA NA NA NA
Broadway-Slavic Village 8,256 5,373 −2,883 −35% NA NA NA NA NA NA
Brooklyn Centre 2,735 2,106 −629 −23% NA NA NA NA NA NA
Buckeye Shaker 3,471 2,048 −1,423 −41% NA NA NA NA NA NA
Central 4,372 4,717 345 8% NA NA NA NA NA NA
Clark Fulton 3,204 2,102 −1,102 −34% NA NA NA NA NA NA
Collinwood Nottingham 4,556 2,518 −2,038 −45% NA NA NA NA NA NA
Cudell 2,747 2,199 −548 −20% NA NA NA NA NA NA
Cuyahoga Valley 176 129 −47 −27% NA NA NA NA NA NA
Detroit-Shoreway 3,650 2,472 −1,178 −32% NA NA NA NA NA NA
Downtown 267 295 28 10% NA NA NA NA NA NA
Edgewater 800 685 −115 −14% NA NA NA NA NA NA
Euclid Green 1,552 924 −628 −40% NA NA NA NA NA NA
Fairfax 2,079 1,222 −857 −41% NA NA NA NA NA NA
Glenville 10,878 5,908 −4,970 −46% NA NA NA NA NA NA
Goodrich-Kirtland Park 794 569 −225 −28% NA NA NA NA NA NA
Hopkins 67 128 61 91% NA NA NA NA NA NA
Hough 4,032 2,397 −1,635 −41% NA NA NA NA NA NA
Jefferson 4,054 3,382 −672 −17% NA NA NA NA NA NA
Kamms Corners 5,133 4,587 −546 −11% NA NA NA NA NA NA
Kinsman 3,408 1,812 −1,596 −47% NA NA NA NA NA NA
Lee-Harvard 2,051 1,636 −415 −20% NA NA NA NA NA NA
Lee-Seville 1,274 795 −479 −38% NA NA NA NA NA NA
Mount Pleasant 6,306 3,606 −2,700 −43% NA NA NA NA NA NA
North Shore Collinwood 3,867 2,793 −1,074 −28% NA NA NA NA NA NA
Ohio City 1,854 1,440 −414 −22% NA NA NA NA NA NA
Old Brooklyn 6,725 5,822 −903 −13% NA NA NA NA NA NA
Saint Clair-Superior 3,754 1,479 −2,275 −61% NA NA NA NA NA NA
Stockyards 3,456 2,732 −724 −21% NA NA NA NA NA NA
Tremont 2,356 1,410 −946 −40% NA NA NA NA NA NA
Union-Miles Park 7,154 3,680 −3,474 −49% NA NA NA NA NA NA
University Circle 529 247 −282 −53% NA NA NA NA NA NA
West Boulevard 5,259 4,531 −728 −14% NA NA NA NA NA NA
Woodland Hills 3,018 1,697 −1,321 −44% NA NA NA NA NA NA
Total 117,101 80,298 −36,803 −31% 67,636 −12,662 −16% 64,554 −3,082 −5%
Changing 15 to 24 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,589 1,629 40 3% NA NA NA NA NA NA
Broadway-Slavic Village 4,033 3,419 −614 −15% NA NA NA NA NA NA
Brooklyn Centre 1,471 1,406 −65 −4% NA NA NA NA NA NA
Buckeye Shaker 1,929 1,678 −251 −13% NA NA NA NA NA NA
Central 2,012 2,133 121 6% NA NA NA NA NA NA
Clark Fulton 1,671 1,508 −163 −10% NA NA NA NA NA NA
Collinwood Nottingham 2,104 1,986 −118 −6% NA NA NA NA NA NA
Cudell 1,508 1,503 −5 −0% NA NA NA NA NA NA
Cuyahoga Valley 162 412 250 154% NA NA NA NA NA NA
Detroit-Shoreway 1,932 1,664 −268 −14% NA NA NA NA NA NA
Downtown 1,576 2,445 869 55% NA NA NA NA NA NA
Edgewater 713 690 −23 −3% NA NA NA NA NA NA
Euclid Green 842 671 −171 −20% NA NA NA NA NA NA
Fairfax 1,052 886 −166 −16% NA NA NA NA NA NA
Glenville 5,561 4,386 −1,175 −21% NA NA NA NA NA NA
Goodrich-Kirtland Park 636 569 −67 −11% NA NA NA NA NA NA
Hopkins 38 100 62 163% NA NA NA NA NA NA
Hough 1,880 1,662 −218 −12% NA NA NA NA NA NA
Jefferson 2,111 2,148 37 2% NA NA NA NA NA NA
Kamms Corners 2,283 2,435 152 7% NA NA NA NA NA NA
Kinsman 1,507 1,095 −412 −27% NA NA NA NA NA NA
Lee-Harvard 1,130 1,212 82 7% NA NA NA NA NA NA
Lee-Seville 629 604 −25 −4% NA NA NA NA NA NA
Mount Pleasant 3,198 2,663 −535 −17% NA NA NA NA NA NA
North Shore Collinwood 1,763 1,907 144 8% NA NA NA NA NA NA
Ohio City 1,107 1,017 −90 −8% NA NA NA NA NA NA
Old Brooklyn 3,538 3,912 374 11% NA NA NA NA NA NA
Saint Clair-Superior 1,715 1,288 −427 −25% NA NA NA NA NA NA
Stockyards 1,836 1,839 3 0% NA NA NA NA NA NA
Tremont 1,321 1,061 −260 −20% NA NA NA NA NA NA
Union-Miles Park 3,412 3,029 −383 −11% NA NA NA NA NA NA
University Circle 3,898 4,006 108 3% NA NA NA NA NA NA
West Boulevard 2,857 3,010 153 5% NA NA NA NA NA NA
Woodland Hills 1,416 990 −426 −30% NA NA NA NA NA NA
Total 64,556 61,044 −3,512 −5% 50,100 −10,944 −18% 48,566 −1,534 −3%
Changing 25 to 34 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 2,250 1,733 −517 −23% NA NA NA NA NA NA
Broadway-Slavic Village 4,712 2,986 −1,726 −37% NA NA NA NA NA NA
Brooklyn Centre 1,624 1,204 −420 −26% NA NA NA NA NA NA
Buckeye Shaker 2,685 1,665 −1,020 −38% NA NA NA NA NA NA
Central 1,342 1,653 311 23% NA NA NA NA NA NA
Clark Fulton 1,609 1,165 −444 −28% NA NA NA NA NA NA
Collinwood Nottingham 2,343 1,383 −960 −41% NA NA NA NA NA NA
Cudell 1,936 1,372 −564 −29% NA NA NA NA NA NA
Cuyahoga Valley 357 375 18 5% NA NA NA NA NA NA
Detroit-Shoreway 2,159 1,717 −442 −20% NA NA NA NA NA NA
Downtown 1,782 3,109 1,327 74% NA NA NA NA NA NA
Edgewater 1,694 1,300 −394 −23% NA NA NA NA NA NA
Euclid Green 808 602 −206 −25% NA NA NA NA NA NA
Fairfax 863 597 −266 −31% NA NA NA NA NA NA
Glenville 4,635 2,934 −1,701 −37% NA NA NA NA NA NA
Goodrich-Kirtland Park 789 596 −193 −24% NA NA NA NA NA NA
Hopkins 46 109 63 137% NA NA NA NA NA NA
Hough 1,627 1,208 −419 −26% NA NA NA NA NA NA
Jefferson 3,132 2,299 −833 −27% NA NA NA NA NA NA
Kamms Corners 4,340 3,600 −740 −17% NA NA NA NA NA NA
Kinsman 1,267 810 −457 −36% NA NA NA NA NA NA
Lee-Harvard 1,093 827 −266 −24% NA NA NA NA NA NA
Lee-Seville 575 417 −158 −27% NA NA NA NA NA NA
Mount Pleasant 2,989 1,834 −1,155 −39% NA NA NA NA NA NA
North Shore Collinwood 2,871 1,606 −1,265 −44% NA NA NA NA NA NA
Ohio City 1,499 1,681 182 12% NA NA NA NA NA NA
Old Brooklyn 6,266 4,498 −1,768 −28% NA NA NA NA NA NA
Saint Clair-Superior 1,536 758 −778 −51% NA NA NA NA NA NA
Stockyards 1,804 1,392 −412 −23% NA NA NA NA NA NA
Tremont 1,660 1,822 162 10% NA NA NA NA NA NA
Union-Miles Park 3,169 1,909 −1,260 −40% NA NA NA NA NA NA
University Circle 1,463 1,134 −329 −22% NA NA NA NA NA NA
West Boulevard 3,422 2,715 −707 −21% NA NA NA NA NA NA
Woodland Hills 1,280 841 −439 −34% NA NA NA NA NA NA
Total 71,847 53,996 −17,851 −25% 62,334 8,338 15% 63,832 1,498 2%
Changing 35 to 44 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 2,459 1,827 −632 −26% NA NA NA NA NA NA
Broadway-Slavic Village 4,897 2,829 −2,068 −42% NA NA NA NA NA NA
Brooklyn Centre 1,670 1,169 −501 −30% NA NA NA NA NA NA
Buckeye Shaker 2,510 1,526 −984 −39% NA NA NA NA NA NA
Central 1,355 957 −398 −29% NA NA NA NA NA NA
Clark Fulton 1,676 1,113 −563 −34% NA NA NA NA NA NA
Collinwood Nottingham 2,489 1,404 −1,085 −44% NA NA NA NA NA NA
Cudell 1,643 1,280 −363 −22% NA NA NA NA NA NA
Cuyahoga Valley 323 173 −150 −46% NA NA NA NA NA NA
Detroit-Shoreway 2,105 1,573 −532 −25% NA NA NA NA NA NA
Downtown 1,182 1,181 −1 −0% NA NA NA NA NA NA
Edgewater 1,115 920 −195 −17% NA NA NA NA NA NA
Euclid Green 1,027 557 −470 −46% NA NA NA NA NA NA
Fairfax 1,137 637 −500 −44% NA NA NA NA NA NA
Glenville 5,670 2,859 −2,811 −50% NA NA NA NA NA NA
Goodrich-Kirtland Park 672 537 −135 −20% NA NA NA NA NA NA
Hopkins 60 78 18 30% NA NA NA NA NA NA
Hough 2,222 1,192 −1,030 −46% NA NA NA NA NA NA
Jefferson 3,362 2,495 −867 −26% NA NA NA NA NA NA
Kamms Corners 4,435 3,584 −851 −19% NA NA NA NA NA NA
Kinsman 1,246 718 −528 −42% NA NA NA NA NA NA
Lee-Harvard 1,557 1,119 −438 −28% NA NA NA NA NA NA
Lee-Seville 785 524 −261 −33% NA NA NA NA NA NA
Mount Pleasant 3,488 1,995 −1,493 −43% NA NA NA NA NA NA
North Shore Collinwood 3,156 2,102 −1,054 −33% NA NA NA NA NA NA
Ohio City 1,408 1,105 −303 −22% NA NA NA NA NA NA
Old Brooklyn 5,845 4,779 −1,066 −18% NA NA NA NA NA NA
Saint Clair-Superior 1,722 816 −906 −53% NA NA NA NA NA NA
Stockyards 1,776 1,334 −442 −25% NA NA NA NA NA NA
Tremont 1,413 1,084 −329 −23% NA NA NA NA NA NA
Union-Miles Park 3,878 2,173 −1,705 −44% NA NA NA NA NA NA
University Circle 651 368 −283 −43% NA NA NA NA NA NA
West Boulevard 3,435 2,637 −798 −23% NA NA NA NA NA NA
Woodland Hills 1,258 745 −513 −41% NA NA NA NA NA NA
Total 73,822 49,555 −24,267 −33% 43,901 −5,654 −11% 44,705 804 2%
Changing 45 to 54 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,829 2,251 422 23% NA NA NA NA NA NA
Broadway-Slavic Village 3,355 3,430 75 2% NA NA NA NA NA NA
Brooklyn Centre 1,200 1,354 154 13% NA NA NA NA NA NA
Buckeye Shaker 2,097 2,028 −69 −3% NA NA NA NA NA NA
Central 960 1,233 273 28% NA NA NA NA NA NA
Clark Fulton 1,074 1,178 104 10% NA NA NA NA NA NA
Collinwood Nottingham 1,749 1,797 48 3% NA NA NA NA NA NA
Cudell 1,254 1,372 118 9% NA NA NA NA NA NA
Cuyahoga Valley 169 169 0 0% NA NA NA NA NA NA
Detroit-Shoreway 1,610 1,697 87 5% NA NA NA NA NA NA
Downtown 714 1,241 527 74% NA NA NA NA NA NA
Edgewater 739 904 165 22% NA NA NA NA NA NA
Euclid Green 959 796 −163 −17% NA NA NA NA NA NA
Fairfax 984 914 −70 −7% NA NA NA NA NA NA
Glenville 4,442 4,071 −371 −8% NA NA NA NA NA NA
Goodrich-Kirtland Park 589 744 155 26% NA NA NA NA NA NA
Hopkins 41 88 47 115% NA NA NA NA NA NA
Hough 1,689 1,806 117 7% NA NA NA NA NA NA
Jefferson 2,246 2,721 475 21% NA NA NA NA NA NA
Kamms Corners 3,216 3,856 640 20% NA NA NA NA NA NA
Kinsman 1,034 928 −106 −10% NA NA NA NA NA NA
Lee-Harvard 1,495 1,547 52 3% NA NA NA NA NA NA
Lee-Seville 671 657 −14 −2% NA NA NA NA NA NA
Mount Pleasant 2,820 2,586 −234 −8% NA NA NA NA NA NA
North Shore Collinwood 2,297 2,764 467 20% NA NA NA NA NA NA
Ohio City 1,238 1,239 1 0% NA NA NA NA NA NA
Old Brooklyn 4,160 5,245 1,085 26% NA NA NA NA NA NA
Saint Clair-Superior 1,155 1,157 2 0% NA NA NA NA NA NA
Stockyards 1,241 1,414 173 14% NA NA NA NA NA NA
Tremont 1,039 1,213 174 17% NA NA NA NA NA NA
Union-Miles Park 3,044 2,864 −180 −6% NA NA NA NA NA NA
University Circle 608 510 −98 −16% NA NA NA NA NA NA
West Boulevard 2,318 2,744 426 18% NA NA NA NA NA NA
Woodland Hills 915 998 83 9% NA NA NA NA NA NA
Total 55,111 59,726 4,615 8% 42,857 −16,869 −28% 41,195 −1,662 −4%
Changing 55 to 64 yrs Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Bellaire-Puritas 1,211 1,544 333 27% NA NA NA NA NA NA
Broadway-Slavic Village 2,165 2,348 183 8% NA NA NA NA NA NA
Brooklyn Centre 712 916 204 29% NA NA NA NA NA NA
Buckeye Shaker 1,206 1,695 489 41% NA NA NA NA NA NA
Central 622 933 311 50% NA NA NA NA NA NA
Clark Fulton 670 772 102 15% NA NA NA NA NA NA
Collinwood Nottingham 1,107 1,254 147 13% NA NA NA NA NA NA
Cudell 697 931 234 34% NA NA NA NA NA NA
Cuyahoga Valley 55 81 26 47% NA NA NA NA NA NA
Detroit-Shoreway 966 1,226 260 27% NA NA NA NA NA NA
Downtown 354 727 373 105% NA NA NA NA NA NA
Edgewater 419 656 237 57% NA NA NA NA NA NA
Euclid Green 490 715 225 46% NA NA NA NA NA NA
Fairfax 745 804 59 8% NA NA NA NA NA NA
Glenville 2,897 3,175 278 10% NA NA NA NA NA NA
Goodrich-Kirtland Park 388 619 231 60% NA NA NA NA NA NA
Hopkins 48 67 19 40% NA NA NA NA NA NA
Hough 1,107 1,412 305 28% NA NA NA NA NA NA
Jefferson 1,310 1,815 505 39% NA NA NA NA NA NA
Kamms Corners 2,080 2,941 861 41% NA NA NA NA NA NA
Kinsman 769 753 −16 −2% NA NA NA NA NA NA
Lee-Harvard 1,405 1,376 −29 −2% NA NA NA NA NA NA
Lee-Seville 614 553 −61 −10% NA NA NA NA NA NA
Mount Pleasant 1,834 2,106 272 15% NA NA NA NA NA NA
North Shore Collinwood 1,380 2,370 990 72% NA NA NA NA NA NA
Ohio City 673 1,166 493 73% NA NA NA NA NA NA
Old Brooklyn 2,698 3,765 1,067 40% NA NA NA NA NA NA
Saint Clair-Superior 740 729 −11 −1% NA NA NA NA NA NA
Stockyards 888 859 −29 −3% NA NA NA NA NA NA
Tremont 614 803 189 31% NA NA NA NA NA NA
Union-Miles Park 2,506 2,277 −229 −9% NA NA NA NA NA NA
University Circle 500 522 22 4% NA NA NA NA NA NA
West Boulevard 1,400 1,841 441 32% NA NA NA NA NA NA
Woodland Hills 631 725 94 15% NA NA NA NA NA NA
Total 35,987 44,700 8,713 24% 51,614 6,914 15% 49,383 −2,231 −4%
Changing 65+ yr Population in Cleveland, 2000-2023
Decennial and ACS censuses
2000 2010 Δ % 2020 Δ % 2023 Δ %
Show the code
my_split_gt <- function(x, .rpt_group, .var_name, .var_val, .spanner, .title, .year) {
  x |>
    filter(rpt_group == .rpt_group) |>
    mutate(geo = if_else(!!ensym(.var_name) == .var_val, "MAIN", "Other")) |>
    mutate(.by = geo, pct = value / sum(value)) |>
    as_tibble() |>
    summarize(.by = c(geo, rpt_level), across(c(value, pct), sum)) |>
    pivot_wider(names_from = geo, values_from = c(value, pct)) |>
    arrange(rpt_level) |>
    mutate(
      rpt_level = fct_drop(rpt_level),
      value_Total = value_MAIN + value_Other,
      pct_Total = value_Total / sum(value_Total)
    ) |>
    janitor::adorn_totals() |>
    select(
      rpt_level, ends_with("MAIN"), ends_with("Other"), ends_with("Total")
    ) |>
    gt() |>
    gt::cols_align("left", 1) |>
    gt::fmt_number(columns = c(2, 4, 6), decimals = 0) |>
    gt::fmt_percent(columns = c(3, 5, 7), decimals = 1) |>
    gt::tab_spanner(.spanner, 2:3) |>
    gt::tab_spanner("Other", 4:5) |>
    gt::tab_spanner("Total", 6:7) |>
    gt::cols_label(
      rpt_level = "", starts_with("value") ~ "Est.", starts_with("pct") ~ "%"
    ) |>
    gt::tab_header(
      glue("{.title} Population, {.year}"),
      glue("by {.rpt_group}")
    ) |>
    gt::tab_options(heading.align = "left") 
}

Footnotes

  1. “Opinion: Downtown Cleveland’s strategy to broaden appeal sees success”, Crains Cleveland Business. “Cleveland’s downtown population continues to surge”, Cleveland Fox 19 News.↩︎

  2. Downtown Cleveland Inc. commissioned a report, “Downtown Cleveland Market Study Report” (pdf), by the Urban Partners consulting firm. The report was released in Apr 2023. Figures are from Table 1: 15,330 people in 2010, 18,708 people in 2020 (22% increase).↩︎

  3. See the Downtown neighborhood (statistical processing area, SPA) in the data table.↩︎

  4. “There’s Still No Agreement on How Many Clevelanders Actually Live Downtown”, Cleveland Scene, Sep 17, 2024.↩︎

  5. Cleveland’s population plateaued around 1930 at 900K. The peak was 914K in the 1950 census. Between 1960 and 1980 the population declined by a third. The current population is slightly below the 1900 value. See Visual Cleveland at https://visual.clevelandhistory.org/census/.↩︎

  6. 362,670 +/- 62. https://data.census.gov/table/ACSST1Y2023.S0101?q=cleveland,%20oh↩︎

  7. Social Planning Areas (SPAs) were developed in the 1950s to coordinate social services at the neighborhood level. Learn more at the Encyclopedia of Cleveland History. Wikipedia has a nice explanation of how neighborhoods relate to Statistical (or social) Planning Areas.↩︎

  8. From https://data.clevelandohio.gov/, go to the Data Catalog and scroll to Census 2020 Analysis.↩︎

  9. There is a map on page 3 of their report.↩︎